package org.elephas.persistence.audit;

import java.lang.reflect.Field;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;

import org.elephas.persistence.event.PersistenceEvent;
import org.elephas.persistence.event.PreInsertEvent;
import org.elephas.persistence.event.PreUpdateEvent;
import org.elephas.util.HibernateUtils;
import org.hibernate.Hibernate;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * Listens to persistence events and processes entity fields annotated with
 * {@link DateCreated}, {@link DateUpdated} annoations.
 * 
 * @author igor.vaynberg
 * @see DateCreated
 * @see DateUpdated
 */
@Component
public class PersistentEntityAuditor implements ApplicationListener {

	private ConcurrentHashMap<String, EntityMetaData> classNameToMetaDataCache = new ConcurrentHashMap<String, EntityMetaData>();

	private boolean setUpdatedInfoOnCreate = true;

	/**
	 * If set to true both updated* and created* fields are set when an entity
	 * is created
	 * 
	 * @param setUpdatedInfoOnCreate
	 */
	public void setSetUpdatedInfoOnCreate(boolean setUpdatedInfoOnCreate) {
		this.setUpdatedInfoOnCreate = setUpdatedInfoOnCreate;
	}

	public void onApplicationEvent(ApplicationEvent ev) {
		try {
			if (ev instanceof PreInsertEvent) {
				final PreInsertEvent event = (PreInsertEvent) ev;
				final EntityMetaData meta = getMetaData(event);
				final Date timestamp = new Date();
				if (meta.dateCreatedField != null) {
					event.getState()[meta.dateCreatedFieldIndex] = timestamp;
					meta.dateCreatedField.set(HibernateUtils.unproxy(event
							.getEntity()), timestamp);

				}
				if (setUpdatedInfoOnCreate && meta.dateUpdatedField != null) {
					event.getState()[meta.dateUpdatedFieldIndex] = timestamp;
					meta.dateUpdatedField.set(HibernateUtils.unproxy(event
							.getEntity()), timestamp);
				}
			} else if (ev instanceof PreUpdateEvent) {
				final PreUpdateEvent event = (PreUpdateEvent) ev;
				final EntityMetaData meta = getMetaData(event);
				final Date timestamp = new Date();
				if (meta.dateUpdatedField != null) {
					event.getNewState()[meta.dateUpdatedFieldIndex] = timestamp;
					meta.dateUpdatedField.set(HibernateUtils.unproxy(event
							.getEntity()), timestamp);
				}
			}
		} catch (Exception e) {
			throw new RuntimeException("Error processing event: " + ev, e);
		}
	}

	private EntityMetaData getMetaData(PersistenceEvent event) {
		final Class<?> clazz = Hibernate.getClass(event.getEntity());
		final String cacheKey = clazz.getName();
		EntityMetaData meta = classNameToMetaDataCache.get(cacheKey);

		if (meta == null) {
			// create meta data and cache it

			Field dateCreatedField = null;
			Field dateUpdatedField = null;

			Class<?> cursor = clazz;
			while (cursor != null) {
				Field[] fields = cursor.getDeclaredFields();
				for (Field field : fields) {
					if (field.getAnnotation(DateCreated.class) != null) {
						dateCreatedField = field;
					} else if (field.getAnnotation(DateUpdated.class) != null) {
						dateUpdatedField = field;
					}
				}
				cursor = cursor.getSuperclass();
			}

			final String[] props = event.getPropertyNames();
			meta = new EntityMetaData(props, dateCreatedField, dateUpdatedField);
			classNameToMetaDataCache.put(cacheKey, meta);
		}

		return meta;
	}

	/**
	 * Meta data about an entity
	 * 
	 * @author ivaynberg
	 */
	private static class EntityMetaData {
		public int dateCreatedFieldIndex;
		public Field dateCreatedField;

		public int dateUpdatedFieldIndex;
		public Field dateUpdatedField;

		public EntityMetaData(String[] props, Field dateCreatedField,
				Field dateUpdatedField) {
			for (int i = 0; i < props.length; i++) {
				if (props[i].equals(dateCreatedField.getName())) {
					dateCreatedFieldIndex = i;
					this.dateCreatedField = dateCreatedField;
					this.dateCreatedField.setAccessible(true);
				} else if (props[i].equals(dateUpdatedField.getName())) {
					dateUpdatedFieldIndex = i;
					this.dateUpdatedField = dateUpdatedField;
					this.dateUpdatedField.setAccessible(true);
				}
			}
		}

	}

}
